package org.budo.support.spring.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.budo.support.lang.util.ReflectUtil;
import org.budo.support.lang.util.StringUtil;
import org.budo.support.spring.context.aware.BudoApplicationContextAware;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanIsAbstractException;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanNameReference;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.FrameworkServlet;

import lombok.extern.slf4j.Slf4j;

/**
 * @author lmw
 */
@Slf4j
public class SpringUtil {
    private static final String DEFAULT_BEAN_FACTORY_POINTCUT_ADVISOR = "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor";

    private static final Map<ServletContext, ApplicationContext> APPLICATION_CONTEXT_IN_SERVLET_CONTEXT = new ConcurrentHashMap<ServletContext, ApplicationContext>();

    /**
     * 如果一个方法本身，或者其继承覆盖的方法，被添加了指定注解，则返回这个(被覆盖的)方法，接口比超类优先
     */
    public static Method annotationPresentMethod(Method method, Class<? extends Annotation> annotationType) {
        if (null == method || null == annotationType) {
            return null;
        }

        if (method.isAnnotationPresent(annotationType)) {
            return method;
        }

        Class<?> declaringType = method.getDeclaringClass();

        Class<?>[] interfaceTypes = declaringType.getInterfaces(); // 似乎返回的是直接接口，间接的未返回
        if (null != interfaceTypes) {
            for (Class<?> interfaceType : interfaceTypes) {
                Method interfaceMethod = ReflectUtil.findMethod(interfaceType, method.getName(), method.getParameterTypes());
                if (null != interfaceMethod) {
                    Method annotationPresent = annotationPresentMethod(interfaceMethod, annotationType);
                    if (null != annotationPresent) {
                        return annotationPresent;
                    }
                }
            }
        }

        Class<?> superType = declaringType.getSuperclass();
        if (null != superType && !Object.class.equals(superType)) {
            Method superMethod = ReflectUtil.findMethod(superType, method.getName(), method.getParameterTypes());
            if (null != superMethod) {
                Method annotationPresent = annotationPresentMethod(superMethod, annotationType);
                if (null != annotationPresent) {
                    return annotationPresent;
                }
            }
        }

        return null;
    }

    public static <T> T getBeanByTypeAndActualTypes(ApplicationContext applicationContext, Class<T> beanType, Type... actualTypes) {
        Map<String, T> map = applicationContext.getBeansOfType(beanType);
        Set<Entry<String, T>> set = map.entrySet();
        for (Entry<String, T> entry : set) {
            T bean = entry.getValue();
            Class<?> type = bean.getClass();
            while (type.getName().contains("$$EnhancerBySpringCGLIB$$")) {
                type = type.getSuperclass();
            }

            if (Arrays.equals(ReflectUtil.getActualTypeArguments(type), actualTypes)) {
                return bean;
            }
        }

        return null;
    }

    /**
     * 注意，此方法未做缓存
     */
    public static <T> T getBean(Class<T> type) {
        ApplicationContext applicationContext = BudoApplicationContextAware.getInstanceNoNull().getApplicationContext();
        return getBean(applicationContext, type);
    }

    public static Object getBean(String beanName) {
        ApplicationContext applicationContext = BudoApplicationContextAware.getInstanceNoNull().getApplicationContext();
        return getBean(applicationContext, beanName);
    }

    /**
     * NoSuchBean 时返回null
     */
    public static <T> T getBean(ApplicationContext applicationContext, Class<T> type) {
        try {
            return applicationContext.getBean(type);
        } catch (NoSuchBeanDefinitionException e) {
            log.error("#123 getBean error, applicationContext=" + applicationContext + ", type=" + type + ", e=" + e);
            return null;
        } catch (BeanNotOfRequiredTypeException e) {
            log.error("#126 getBean error, applicationContext=" + applicationContext + ", type=" + type + ", e=" + e);
            return null;
        } catch (Throwable e) {
            log.error("#129 getBean error, applicationContext=" + applicationContext + ", type=" + type + ", e=" + e, e);
            return null;
        }
    }

    /**
     * @param ignoreCreation 配置为true时 遇到 BeanCreationException 不打异常
     */
    public static Object getBean(ApplicationContext applicationContext, String beanName, Boolean ignoreCreation) {
        try {
            ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
            DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();

            String beanDefinitionName = beanName;
            if (beanDefinitionName.startsWith("&")) {
                beanDefinitionName = beanDefinitionName.substring(1);
            }

            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
            if (null == beanDefinition) {
                log.warn("#131 beanDefinition is null, beanName=" + beanName + ", beanDefinitionName=" + beanDefinitionName);
                return null;
            }

            if (beanDefinition.isAbstract()) {
                log.warn("#131 beanDefinition isAbstract, beanName=" + beanName + ", beanDefinitionName=" + beanDefinitionName);
                return null;
            }

            return applicationContext.getBean(beanName);
        } catch (BeanIsAbstractException e) {
            log.error("#98 bean is abstract, beanName=" + beanName + ", applicationContext=" + applicationContext + ", e=" + e);
            return null;
        } catch (NoSuchBeanDefinitionException e) {
            log.error("#101 getBean error, applicationContext=" + applicationContext + ", beanName=" + beanName + ", e=" + e);
            return null;
        } catch (BeanCreationException e) {
            if (null != ignoreCreation && ignoreCreation) {
                log.error("#120 getBean error, applicationContext=" + applicationContext + ", beanName=" + beanName + ", e=" + e);
            } else {
                log.error("#122 getBean error, applicationContext=" + applicationContext + ", beanName=" + beanName + ", e=" + e, e);
            }

            return null;
        } catch (IllegalStateException e) {
            if ((null != ignoreCreation && ignoreCreation) //
                    && (("" + e).contains("has not been refreshed yet") //
                            || ("" + e).contains("has been closed already"))) {
                log.error("#128 getBean error, applicationContext=" + applicationContext + ", beanName=" + beanName + ", e=" + e);
                return null;
            }

            log.error("#132 getBean error, applicationContext=" + applicationContext + ", beanName=" + beanName + ", e=" + e, e);
            return null;
        } catch (Throwable e) {
            if (("" + e).contains("No Scope registered for scope name")) {
                log.error("#126 getBean error, applicationContext=" + applicationContext + ", beanName=" + beanName + ", e=" + e);
                return null;
            }

            log.error("#104 getBean error, applicationContext=" + applicationContext + ", beanName=" + beanName + ", e=" + e, e);
            return null;
        }
    }

    public static Object getBean(ApplicationContext applicationContext, String beanName) {
        return getBean(applicationContext, beanName, false);
    }

    public static <T> List<T> getBeanListByType(ApplicationContext applicationContext, Class<T> type) {
        Map<String, T> beansOfType = applicationContext.getBeansOfType(type);
        Collection<T> beans = beansOfType.values();
        return new ArrayList<T>(beans);
    }

    public static ApplicationContext getApplicationContext(HttpServletRequest request) {
        if (null == request) {
            log.error("#162 request is null");
            return null;
        }

        HttpSession session = request.getSession();
        ServletContext servletContext = session.getServletContext();
        ApplicationContext applicationContext = APPLICATION_CONTEXT_IN_SERVLET_CONTEXT.get(servletContext);
        if (null != applicationContext) {
            return applicationContext;
        }

        applicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        if (null != applicationContext) {
            APPLICATION_CONTEXT_IN_SERVLET_CONTEXT.put(servletContext, applicationContext);
            return applicationContext;
        }

        Enumeration<String> enumeration = servletContext.getAttributeNames();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            if (StringUtil.startsWith(name, FrameworkServlet.SERVLET_CONTEXT_PREFIX)) {
                applicationContext = (ApplicationContext) servletContext.getAttribute(name);

                APPLICATION_CONTEXT_IN_SERVLET_CONTEXT.put(servletContext, applicationContext);
                return applicationContext;
            }
        }

        log.error("#198 applicationContext not found, servletContext=" + servletContext + ", request=" + request);
        return null;
    }

    public static String getNameOfBean(ApplicationContext applicationContext, Object beanInstance) {
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            if (applicationContext.getBean(beanName).equals(beanInstance)) {
                return beanName;
            }
        }

        log.info("#79 applicationContext=" + applicationContext + "beanInstance=" + beanInstance);
        return null;
    }

    public static BeanDefinition getBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) {
        if (!beanFactory.containsBeanDefinition(beanName)) {
            return null;
        }
        return beanFactory.getBeanDefinition(beanName);
    }

    public static BeanDefinition getBeanDefinition(ApplicationContext applicationContext, String beanName) {
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();

        if (!beanFactory.containsBeanDefinition(beanName)) {
            return null;
        }

        return beanFactory.getBeanDefinition(beanName);
    }

    public static BeanDefinition getBeanDefinition(BeanDefinitionRegistry beanDefinitionRegistry, String beanName) {
        if (!beanDefinitionRegistry.containsBeanDefinition(beanName)) {
            return null;
        }

        return beanDefinitionRegistry.getBeanDefinition(beanName);
    }

    public static BeanDefinition getBeanDefinition(ApplicationContext applicationContext, Class<?> interfaceType) {
        String[] beanNames = applicationContext.getBeanNamesForType(interfaceType);
        if (null == beanNames || beanNames.length < 1 || beanNames.length > 1) {
            throw new RuntimeException("#246 interfaceType=" + interfaceType + ", beanNames=" + Arrays.toString(beanNames) + ", applicationContext=" + applicationContext);
        }

        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
        return beanFactory.getBeanDefinition(beanNames[0]);
    }

    public static void removeBeanDefinition(String beanName) {
        ApplicationContext applicationContext = BudoApplicationContextAware.getInstanceNoNull().getApplicationContext();
        removeBeanDefinition(applicationContext, beanName);
    }

    public static void removeBeanDefinition(ApplicationContext applicationContext, String beanName) {
        if (null == applicationContext || null == beanName) {
            throw new IllegalArgumentException("#256 applicationContext=" + applicationContext + ", beanName=" + beanName);
        }

        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();

        beanFactory.removeBeanDefinition(beanName);
    }

    public static String getNameByBeanType(String beanType) {
        Class type = ReflectUtil.classForName(beanType);
        return getNameByBeanType(type);
    }

    public static String getNameByBeanType(ListableBeanFactory beanFactory, String beanType) {
        Class type = ReflectUtil.classForName(beanType);
        return getNameByBeanType(beanFactory, type);
    }

    public static String getNameByBeanType(Class<?> beanType) {
        ApplicationContext applicationContext = BudoApplicationContextAware.getInstanceNoNull().getApplicationContext();

        return getNameByBeanType(applicationContext, beanType);
    }

    public static String getNameByBeanType(ListableBeanFactory beanFactory, Class<?> beanType) {
        String[] beanNames = beanFactory.getBeanNamesForType(beanType);

        if (null == beanNames || beanNames.length < 1 || beanNames.length > 1) {
            throw new RuntimeException("#282 beanType=" + beanType + ", beanNames=" + Arrays.toString(beanNames));
        }

        return beanNames[0];
    }

    public static List<BeanDefinition> getBeanDefinitionListByType(BeanDefinitionRegistry registry, String beanType) {
        List<BeanDefinition> list = new ArrayList<BeanDefinition>();

        String[] beanDefinitionNames = registry.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = registry.getBeanDefinition(beanDefinitionName);
            String beanClassName = beanDefinition.getBeanClassName();
            if (StringUtil.equals(beanClassName, beanType)) {
                list.add(beanDefinition);
            }
        }

        return list;
    }

    public static BeanDefinition getAdvisorBeanDefinitionByAdviceType(BeanDefinitionRegistry beanDefinitionRegistry, String beanType) {
        List<BeanDefinition> advisorDefinitions = getBeanDefinitionListByType(beanDefinitionRegistry, DEFAULT_BEAN_FACTORY_POINTCUT_ADVISOR); // 所有Aop切面配置
        for (BeanDefinition advisorDefinition : advisorDefinitions) {
            MutablePropertyValues mutablePropertyValues = advisorDefinition.getPropertyValues();
            Object adviceBeanName = mutablePropertyValues.get("adviceBeanName");

            if (!(adviceBeanName instanceof RuntimeBeanNameReference)) {
                continue;
            }

            String beanName = ((RuntimeBeanNameReference) adviceBeanName).getBeanName();
            BeanDefinition adviceDefinition = getBeanDefinition(beanDefinitionRegistry, beanName);
            if (null == adviceDefinition) {
                continue;
            }

            String beanClassName = adviceDefinition.getBeanClassName();
            if (StringUtil.equals(beanClassName, beanType)) {
                return advisorDefinition;
            }
        }

        log.info("#354 return null, beanType=" + beanType + ", beanDefinitionRegistry=" + beanDefinitionRegistry);
        return null;
    }
}